---
type: tutorial
title: '可选：创建内容集合'
description: |-
  教程：构建你的第一个 Astro 博客 — 
  将你的博客从基于文件的路由转换为内容集合
i18nReady: true
head:
  - tag: title
    content: 搭建博客教程：创建内容集合 | Docs
---
import PackageManagerTabs from '~/components/tabs/PackageManagerTabs.astro';
import Box from '~/components/tutorial/Box.astro';
import Checklist from '~/components/Checklist.astro';
import MultipleChoice from '~/components/tutorial/MultipleChoice.astro';
import PreCheck from '~/components/tutorial/PreCheck.astro';
import Option from '~/components/tutorial/Option.astro';
import { Steps } from '@astrojs/starlight/components';

现在你已经拥有了一个使用 Astro [内置的基于文件路由](/zh-cn/guides/routing/#静态路由) 的博客，你将要更新博客以使用 [内容集合](/zh-cn/guides/content-collections/)。内容集合是一个管理一组相似内容（例如博客文章）的强大方法。

<PreCheck>
  - 将你的博客文章文件夹移至 `src/blog/`
  - 创建一个模式（schema）来定义你的博客文章的 frontmatter
  - 使用 `getCollection()` 来获取博客文章内容和元数据
</PreCheck>

## 了解：页面 vs 集合

即使是使用了内容集合，你也仍然可以通过 `src/pages/` 文件夹来单独创建页面，例如 **关于** 页面。但是，将你的博客文章移出该特殊文件夹之外，能够允许你使用更加强大也更具性能的 API，从而生成你博客文章的索引并且展示独立的博客文章。

与此同时，你将能够在你的代码编辑器中获得更好地引导和自动补全，因为你将拥有一个用于为每篇文章定义通用结构的 **[模式](/zh-cn/guides/content-collections/#定义集合模式schema)**，通过它 Astro 将会帮助你使用 [Zod](https://zod.dev/)，一个 TypeScript 的模式声明和验证库来实现这一切。在你的模式中，你可以指定在什么时候哪些 frontmatter 属性是必要的，例如描述信息或是作者信息，而这些信息的数据类型又应该是什么样的，例如是字符串或是数组。这样就可以更快速的捕捉到错误，并用带有描述性的错误消息告诉你确切的问题是什么。

在我们的指南中，阅读有关 [Astro 内容集合](/zh-cn/guides/content-collections/) 的更多信息，或者从下面的教学步骤开始，以将基础的博客从 `src/pages/posts/` 转换为 `src/blog/`。

<Box icon="question-mark">
### 检验你的知识

1. 你可能会在 `src/pages/` 中保留哪种类型的页面？

    <MultipleChoice>
      <Option>
        所有包含相同基本结构和元数据的博客文章
      </Option>
      <Option>
        一个电子商务网站的产品页面
      </Option>
      <Option isCorrect>
        一个单独的联系方式页面，因为你没有该类型的多个相似页面
      </Option>
    </MultipleChoice>

2. 哪个 **不是** 将博客文章转移到内容集合的优势？

    <MultipleChoice>
      <Option isCorrect>
        页面是基于每个文件自动创建的
      </Option>
      <Option>
        具有更好的错误信息，因为 Astro 对每个文件都有更多了解
      </Option>
      <Option>
        通过更强大的函数得到更好的数据获取
      </Option>
    </MultipleChoice>

3. 内容集合使用 TypeScript ...
    <MultipleChoice>
      <Option>
        让我感到难过
      </Option>
      <Option isCorrect>
        了解和验证我的集合，并提供编辑器工具
      </Option>
      <Option>
        只有当我在 `tsconfig.json` 中设置了 `strictest`​​ 配置时才奏效
      </Option>
    </MultipleChoice>

</Box>

下面的步骤向你展示了如何通过为博客文章创建内容集合来扩展构建博客教程的最终产品。

## 升级依赖

升级到最新版本的 Astro，并通过在终端中运行以下命令来升级所有集成：

    <PackageManagerTabs>
      <Fragment slot="npm">
      ```shell
      # 将 Astro 和官方集成一并升级
      npx @astrojs/upgrade
      ```
      </Fragment>
      <Fragment slot="pnpm">
      ```shell
      # 将 Astro 和官方集成一并升级
      pnpm dlx @astrojs/upgrade
      ```
      </Fragment>
      <Fragment slot="yarn">
      ```shell
      # 将 Astro 和官方集成一并升级
      yarn dlx @astrojs/upgrade
      ```
      </Fragment>
    </PackageManagerTabs>

## 为你的文章创建集合

<Steps>
1. 创建一个新的 **集合**（文件夹）并命名为 `src/blog/`。

2. 将你现有的所有博客文章（`.md` 文件）从 `src/pages/posts/` 移至新集合中。

3. 创建一个 `src/content.config.ts` 文件用于为你的 `postsCollection` [定义模式](/zh-cn/guides/content-collections/#定义集合模式schema)。对于现有的博客教程代码，将以下内容添加到文件中，以定义其博客文章中使用的所有 frontmatter 属性：

    ```ts title="src/content.config.ts"
    // 导入 glob 加载器（loader）
    import { glob } from "astro/loaders";
    // 从 `astro:content` 导入工具函数
    import { z, defineCollection } from "astro:content";
    // 为每个集合定义一个 `loader` 和 `schema`
    const blog = defineCollection({
        loader: glob({ pattern: '**/[^_]*.md', base: "./src/blog" }),
        schema: z.object({
          title: z.string(),
          pubDate: z.date(),
          description: z.string(),
          author: z.string(),
          image: z.object({
            url: z.string(),
            alt: z.string()
          }),
          tags: z.array(z.string())
        })
    });
    // 导出一个单独的 `collections` 对象用以注册你的集合（们）
    export const collections = { blog };
    ```

4. 为了使 Astro 识别你的模式，请退出（`CTRL + C`）并重启开发服务器以继续使用教程。这将定义 `astro:content` 模块。
</Steps>

## 从集合中生成页面

<Steps>
1. 创建一个页面文件并命名为 `src/pages/posts/[...slug].astro`。当你的 Markdown 和 MDX 文件在集合中时，将不再使用 Astro 的基于文件的路由自动生成为页面，因此你必须创建一个页面，用于负责生成每个单独的博客文章。

2. 添加以下代码以 [查询你的集合](/zh-cn/guides/content-collections/#查询集合)，从而使每个博客文章的 slug 和页面内容都可以生成：

    ```astro title="src/pages/posts/[...slug].astro"
    ---
    import { getCollection, render } from 'astro:content';

    export async function getStaticPaths() {
      const posts = await getCollection('blog');
      return posts.map(post => ({
        params: { slug: post.id }, props: { post },
      }));
    }

    const { post } = Astro.props;
    const { Content } = await render(post);
    ---
    ```

3. 在 Markdown 页面的布局中渲染你的文章 `<Content />`。这使得你可以为所有文章指定一个通用布局。

    ```astro title="src/pages/posts/[...slug].astro" ins={3,15-17}
    ---
    import { getCollection, render } from 'astro:content';
    import MarkdownPostLayout from '../../layouts/MarkdownPostLayout.astro';

    export async function getStaticPaths() {
      const posts = await getCollection('blog');
      return posts.map(post => ({
        params: { slug: post.id }, props: { post },
      }));
    }

    const { post } = Astro.props;
    const { Content } = await render(post);
    ---
    <MarkdownPostLayout frontmatter={post.data}>
      <Content />
    </MarkdownPostLayout>
    ```

4. 移除每个单独的文章 frontmatter 中定义的 `layout`。你的内容在渲染时将被包装在布局中，因此已不再需要该属性。

    ```md title="src/content/posts/post-1.md" del={2}
    ---
    layout: ../../layouts/MarkdownPostLayout.astro
    title: '我的第一篇博客文章'
    pubDate: 2022-07-01
    ...
    ---
    ```
</Steps>

## 用 `getCollection()` 代替 `import.meta.glob()`

<Steps>
5. 任何有博客文章列表的地方，例如教程中的 Blog 页面（`src/pages/blog.astro/`），你都需要用 [`getCollection()`](/zh-cn/reference/modules/astro-content/#getcollection) 来代替 `import.meta.glob()` 以此作为从 Markdown 文件中获取内容和元数据的方式。

    ```astro title="src/pages/blog.astro" "post.data" "getCollection(\"blog\")" "/posts/${post.id}/" del={7} ins={2,8}
    ---
    import { getCollection } from "astro:content";
    import BaseLayout from "../layouts/BaseLayout.astro";
    import BlogPost from "../components/BlogPost.astro";

    const pageTitle = "我的 Astro 学习博客";
    const allPosts = Object.values(import.meta.glob("../pages/posts/*.md", { eager: true }));
    const allPosts = await getCollection("blog");
    ---
    ```

6. 你还需要更新对每个 `post` 所返回数据的引用。现在，你将在每个对象的 `data` 属性上找到 frontmatter。另外，当使用集合时，每个 `post` 对象会有一个页面 `slug`，而不是一个完整的 URL。

    ```astro title="src/pages/blog.astro" "data" "/posts/$\{post.id\}/" del={14} ins={15}
    ---
    import { getCollection } from "astro:content";
    import BaseLayout from "../layouts/BaseLayout.astro";
    import BlogPost from "../components/BlogPost.astro";

    const pageTitle = "我的 Astro 学习博客";
    const allPosts = await getCollection("blog");
    ---
    <BaseLayout pageTitle={pageTitle}>
      <p>在这里，我将分享我的 Astro 学习之旅。</p>
      <ul>
        {
          allPosts.map((post) => (
            <BlogPost url={post.url} title={post.frontmatter.title} />)}
            <BlogPost url={`/posts/${post.id}/`} title={post.data.title} />
          ))
        }
      </ul>
    </BaseLayout> 
    ```

7. 教程博客项目也使用 `src/pages/tags/[tag].astro` 为每个标签动态地生成了一个页面，并将标签列表展示在了 `src/pages/tags/index.astro`。
   
          将与上述相同的更改应用于这两个文件：
      
          - 使用 `getCollection("blog")` 来获取有关你所有博客文章的数据，而不是使用 `import.meta.glob()`
          - 使用 `data` 来获取所有 frontmatter 值，而不是使用 `frontmatter` 
          - 通过将文章的 `slug` 添加到 `/posts/` 路径来创建页面 URL
        
        生成包含某个标签的文章页面现在变为：

        ```astro title="src/pages/tags/[tag].astro" "post.data.tags" "getCollection(\"blog\")" "post.data.title" ins={2} "/posts/${post.id}/"
        ---
        import { getCollection } from "astro:content";
        import BaseLayout from "../../layouts/BaseLayout.astro";
        import BlogPost from "../../components/BlogPost.astro";

        export async function getStaticPaths() {
          const allPosts = await getCollection("blog");
          const uniqueTags = [...new Set(allPosts.map((post) => post.data.tags).flat())];

          return uniqueTags.map((tag) => {
            const filteredPosts = allPosts.filter((post) =>
              post.data.tags.includes(tag)
            );
            return {
              params: { tag },
              props: { posts: filteredPosts },
            };
          });
        }
        
        const { tag } = Astro.params;
        const { posts } = Astro.props;
        ---

        <BaseLayout pageTitle={tag}>
          <p>包含「{tag}」标签的文章</p>
          <ul>
            { posts.map((post) => <BlogPost url={`/posts/${post.id}/`} title={post.data.title} />) }
          </ul>
        </BaseLayout>
        ```

        <Box icon="puzzle-piece">
          ### 亲自尝试 - 更新标签索引页面的查询

          导入并使用 `getCollection` 来获取 `src/pages/tags/index.astro` 中博客文章所使用的标签，依照 [与上述相同的步骤](#用-getcollection-代替-importmetaglob)。

          <details>
          <summary>给我看看代码！</summary>
          ```astro title="src/pages/tags/index.astro" "post.data" "getCollection(\"blog\")" ins={2}
          ---
          import { getCollection } from "astro:content";
          import BaseLayout from "../../layouts/BaseLayout.astro";     
          const allPosts = await getCollection("blog");
          const tags = [...new Set(allPosts.map((post) => post.data.tags).flat())];
          const pageTitle = "标签索引";
          ---
          <!-- ... -->
          ```
          </details>
      </Box>
</Steps>

## 更新任意 frontmatter 值以匹配模式

如有必要，请在整个项目中更新任意 frontmatter，例如在你的布局中，那些与你的集合模式不符的部分。

在博客教程示例中，`pubDate` 是字符串类型。现在，根据模式对文章 frontmatter 定义的类型，`pubDate` 将会是一个 `Date` 对象。现在，你可以利用这一点使用任何 `Date` 对象可用的方法来格式化日期。

要在博客文章布局中渲染日期，请使用 `toLocaleDateString()` 方法将其转换为字符串：

```astro title="src/layouts/MarkdownPostLayout.astro" ins="toString()"
<!-- ... -->
<BaseLayout pageTitle={frontmatter.title}>
    <p>{frontmatter.pubDate.toLocaleDateString()}</p>
    <p><em>{frontmatter.description}</em></p>
    <p>作者：{frontmatter.author}</p>
    <img src={frontmatter.image.url} width="300" alt={frontmatter.image.alt} />
<!-- ... -->
```

## 更新 RSS 函数

教程博客项目包含了 RSS 订阅。此功能还必须使用 `getCollection()` 以从博客文章中返回信息。然后，你将使用返回的 `data` 对象生成 RSS 订阅项。

    ```js title="src/pages/rss.xml.js" del={2,11} ins={3,6,12-17}
    import rss from '@astrojs/rss';
    import { pagesGlobToRssItems } from '@astrojs/rss';
    import { getCollection } from 'astro:content';

    export async function GET(context) {
      const posts = await getCollection("blog");
      return rss({
        title: 'Astro Learner | Blog',
        description: 'My journey learning Astro',
        site: context.site,
        items: await pagesGlobToRssItems(import.meta.glob('./**/*.md')),
        items: posts.map((post) => ({
          title: post.data.title,
          pubDate: post.data.pubDate,
          description: post.data.description,
          link: `/posts/${post.id}/`,
        })),
        customData: `<language>en-us</language>`,
      })
    }
    ```

有关使用内容集合的博客教程的完整示例，请参阅教程存储库中的 [Content Collections 分支](https://github.com/withastro/blog-tutorial-demo/tree/content-collections)。

<Box icon="check-list">

## 任务清单
<Checklist>
- [ ] 我可以使用内容集合来管理一组相似内容，从而提高性能并优化组织。
</Checklist>
</Box>
